module DryScaffold
#  @@scaffold_template_dir = "#{File.dirname(__FILE__)}/../views"
  class ScaffoldTable #:nodoc:
    def initialize(klass, options)
      
    end
  end
  
  class ScaffoldColumn #:nodoc:
    @@fields_in_column_target = 0
    @@fields_in_column_target_cnt = 0
   
    attr_reader :ajax, :auto_complete, :combine_n_fields_in_column, :description, :eval, :filterable, :filter_value, :in_edit, :in_list, :in_show, :lookup_eval, :max_group_by, :max_length, :name, :size_in_edit, :size_in_list, :sortable, :sort_order, :sort_sql, :sub_type, :eval_default_filter, :eval_restrict_to_filter, :type
    attr_writer :filter_value, :sort_order
    
    def initialize(klass, options) #:nodoc:
      @name = options[:name]
      @eval = options[:eval].nil? ? nil : options[:eval]
      @eval_restrict_to_filter = options[:eval_restrict_to_filter].nil? ? nil : options[:eval_restrict_to_filter]
      @eval_default_filter = options[:eval_default_filter].nil? ? nil : options[:eval_default_filter]
      @sub_type = options[:sub_type].nil? ? nil : options[:sub_type]
      @in_list = options[:in_list].nil? ? false : options[:in_list]      
      @in_edit = options[:in_edit].nil? ? true : options[:in_edit]      
      @in_show = options[:in_show].nil? ? @in_edit : options[:in_show]      
      @label = options[:label].nil? ? Inflector.titleize(@name) : options[:label]      
      @lookup_eval = options[:lookup_eval].nil? ? nil : options[:lookup_eval]
      @sortable = options[:sortable].nil? ? false : options[:sortable]      
      @sort_sql = options[:sort_sql].nil? ? klass.table_name + "." + @name : options[:sort_sql] unless !@sortable
      @ajax = options[:ajax].nil? ? false : options[:ajax]
      @auto_complete = options[:auto_complete].nil? ? false : options[:auto_complete]
      @description = options[:description].nil? ? '' : options[:description]
      @combine_n_fields_in_column = options[:combine_n_fields_in_column].nil? ? 0 : options[:combine_n_fields_in_column]
      @filterable = options[:filterable].nil? ? false : options[:filterable]
      @filter_value = options[:filter_value].nil? ? '' : options[:filter_value]
      @max_group_by = options[:max_group_by].nil? ? '' : options[:max_group_by]
      @sort_order = options[:sort_order].nil? ? '' : options[:sort_order]
      @size_in_list = options[:size_in_list].nil? ? 15 : options[:size_in_list]      
      @size_in_edit = options[:size_in_edit].nil? ? 40 : options[:size_in_edit]      
      @max_length = options[:max_length].nil? ? @size_in_edit : options[:max_length]
      
      if options.has_key?(:type)
        @type = options[:type]
      elsif name.include?("phone") || name == "fax"
        if name.include?("ext")
          @type = :phone_ext
          if @in_edit != false
            @size_in_edit = 10
            @max_length = 10
          end
        else
          @type = :phone
          if @in_edit != false
            @size_in_edit = 12
            @max_length = 12
          end
        end
      else
        @type = klass.columns_hash[options[:name]].type
      end
    end
    
    def sortable? 
      @sortable
    end
    
    def viewable?
      @name != "created_at" and @name != "updated_at" and @name != "lock_version"
    end
    
    def viewable_in_edit?
      @in_edit
    end

    def viewable_in_list?
      @in_list
    end

    def viewable_in_show?
      @in_show
    end

    def combining_fields #:nodoc:
      if fields_in_column > 0
        @@fields_in_column_target = fields_in_column
        @@fields_in_column_target_cnt = @@fields_in_column_target - 1
      elsif @@fields_in_column_target > 0
        @@fields_in_column_target_cnt -= 1
      else
        @@fields_in_column_target = 0
      end
      @@fields_in_column_target.to_s
    end

    def size_in_edit
      in_edit
    end

    def size_in_list
      in_list
    end

    def display_type
      type
    end

    def fields_in_column #:nodoc:
      @combine_n_fields_in_column
    end

    def in_list_query? #:nodoc:
      @in_list || @max_group_by ? true : false
    end

    def label
      @label ? @label : ActiveRecord::Base.human_attribute_name(@name)
    end

    def one_column #:nodoc:
      if @@fields_in_column_target > 0
        false
      else
        true
      end
    end

    def start_column #:nodoc:
      if @@fields_in_column_target > 0
        @@fields_in_column_target == fields_in_column ? true : false
      else
        true
      end
    end

    def end_column #:nodoc:
      if @@fields_in_column_target > 0
        if @@fields_in_column_target && @@fields_in_column_target_cnt == 0 
          @@fields_in_column_target = 0
          true
        else
          false
        end
      else
        true
      end
    end

  end
  
  module Model #:nodoc:
    module ClassMethods #:nodoc:
  
    def build_scaffold_table
      ScaffoldTable.new(self, { :sortable => true })
    end    

    def build_scaffold_columns
      scaffold_columns = Array.new
      content_columns.each do |column|
        scaffold_columns << ScaffoldColumn.new(self, { :name => column.name })
      end 
      scaffold_columns
    end    
    
    def build_scaffold_columns_hash
      scaffold_columns_hash = Hash.new
      scaffold_columns.each do |scaffold_column|
        scaffold_columns_hash[scaffold_column.name] = scaffold_column
      end
        scaffold_columns_hash
      end
    end
  end

  module Helper #:nodoc:
  end
  
  module Common #:nodoc:
    def current_sort(params)
      session[params[:scaffold_id]][:sort] rescue ''
    end
  
    def current_sort_direction(params)
      session[params[:scaffold_id]][:sort_direction] rescue ''
    end  
  end 
  
  module Controller #:nodoc:
    include DryScaffold::Common
  
    def clear_flashes 
    end
  
    def store_or_get_from_session(id_key, value_key)
      session[id_key][value_key] = params[value_key] if !params[value_key].nil?
      params[value_key] ||= session[id_key][value_key]
    end
  
    def update_params(options)
      @scaffold_id = params[:scaffold_id] ||= options[:default_scaffold_id]
      session[@scaffold_id] ||= {:sort => options[:default_sort], :sort_direction => options[:default_sort_direction], :page => 1}
    
      store_or_get_from_session(@scaffold_id, :sort)
      store_or_get_from_session(@scaffold_id, :sort_direction)
      store_or_get_from_session(@scaffold_id, :page)
    end
    
  end
  
end

class ActiveRecord::Base
  extend DryScaffold::Model::ClassMethods

  def self.columns_into_sql(options = {}) #:nodoc:
    sql = ""
    for column in @by_model.scaffold_columns
  	# for col_name in @by_model.column_order 
  	# 			column = @by_model.columns_hash[col_name]
			if column.in_list_query?
			  sql << "," if sql.length > 0
        if column.max_group_by
          sql << "MAX("
        end
				sql << @by_model.table_name + "." + column.name
        if column.max_group_by
          sql << ")"
        end
				sql << " as " + column.name
	 	  end 
		end
		sql
  end

  def self.find_by_def(options = {}) #:nodoc:
    @by_model = self
  	@sql_select = columns_into_sql
    if options[:include_children]
    	self.reflect_on_all_associations.to_a.each do |assoc|
        @by_model = assoc.name.to_s.camelize.singularize.constantize
      	@sql_select << "," + columns_into_sql
      	@sql_include = [] if @sql_include.nil?
    	  @sql_include << assoc.name
      end
    end
    options.delete(:include_children)
    options[:include] = @sql_include
  	options[:select] = @sql_select
    join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
    options[:from] = self.table_name + join_dependency.join_associations.collect{|join| join.association_join }.join
    options[:group] = self.table_name + ".id" if @sql_select.include?("MAX(")
    find_by_sql construct_finder_sql(options)
  end

  # Returns a display worthy label of the Model.
  def self.label
    @label ? @label : name.titleize.split.last.pluralize
  end

  # Give it a name other than the name of the Model.
  #
  # Example:
  #   known_to_humans_as "Workshop"
  #
  def self.known_to_humans_as(value)
    @label = value
  end  
  
  # Customize your confirm message in the javascript popup on delete globally in environment.rb or by calling this function
  # in your Model.
  #
  # Example in Model:
  #   confirm_message_on_delete "Are you really sure?"
  #
  # Example in environment.rb:
  #   module DryScaffoldEngine
  #     config :ajax_filter_fields, true
  #     config :confirm_message_on_delete, "Are you sure?"
  #   end  
  def self.confirm_message_on_delete(value)
    @on_delete_message = value
  end
  
  # Confirm message used in popup window on delete.
  #
  # Example in Model:
  #   Company.on_delete_message
  #
  def self.on_delete_message
    message = @on_delete_message
    message ||= DryScaffoldEngine.config(:confirm_message_on_delete) if !DryScaffoldEngine.config(:confirm_message_on_delete).nil? and DryScaffoldEngine.config(:confirm_message_on_delete)
    message ||= 'Are you sure?'
    message
  end
  
  # Identifies which columns should be managed and how to manage them. The <tt>name</tt> of the column in the Model is required.
  # 
  # Options are:
  # * <tt>:ajax</tt> - Default value is +false+. If set to +true+ then in-line AJAX edit is enabled for this column.
  # * <tt>:description</tt> - If supplied, the value is displayed after the edit field when editing.
  # * <tt>:eval</tt> - Can be a +String+ to be evaluated or a +Symbol+ that identifies a method. If a +String+, to access fields within
  #   the model access them via the method +one_row+. For example: <tt>"one_row.contacts.size.to_s"</tt>. If it is a +Symbol+ 
  #   the method must accept +one_row+ as a paramter. For example: <tt>company_type_id_list(one_row)</tt>. See examples below.
  # * <tt>:filterable</tt> - Default value is +false+.
  # * <tt>:filter_value</tt> - Supply an initial filter value.
  # * <tt>:in_edit</tt> - Show this column on Edit. Default value is +true+.
  # * <tt>:in_list</tt> - Show this column in the List view. Default value is +false+.
  # * <tt>:in_show</tt> - Show this column in the Show view. Default value is the value of <tt>:in_edit</tt>.
  # * <tt>:label</tt>
  # * <tt>:lookup_eval</tt> - Can be a String to be evaluated or a Symbol that identifies a method. See <tt>:eval</tt>.
  # * <tt>:max_length</tt> - Maximum length to allow in text field. <tt>text_field ... :maxlength => column.max_length</tt>.
  # * <tt>:sortable</tt> - Default value is +false+. May also assign an initial value of <tt>:asc</tt> or <tt>:desc</tt>.
  # * <tt>:sub_type</tt> - Currently only applies when <tt>:type => :date</tt>.
  # * <tt>:type</tt> - Default value is a text field.
  #   * <tt>:country</tt>
  #   * <tt>:custom</tt> - Assumes the following methods are defined in the +Helper+ file: company_type_id_edit(one_row, column),
  #     company_type_id_list(one_row), company_type_id_filter(one_row, column, filter_by).
  #   * <tt>:date</tt> - if the DhtmlCalendar Plugin is present you can specify the following <tt>:sub_type =></tt>:
  #     * <tt>:box</tt> - Use the DateBocks controls.
  #     * <tt>:field</tt> - Use a text field.
  #     * <tt>:flat</tt> - Calendar always displayed.
  #     * <tt>:popup</tt> - Use an icon to click on only.
  #     * <tt>:select</tt> - Use Rails like drop down controls (Combo-boxes).
  #   * <tt>:lookup</tt> - Expects values for options <tt>:eval</tt> and <tt>:lookup_eval</tt>. See example below.
  #   * <tt>:password</tt>
  #   * <tt>:phone</tt>
  #   * <tt>:state</tt>
  #   * <tt>:text_area</tt>
  #   * <tt>:timezone</tt>
  # Model Examples:
  # 
  #   has_column "id",{
  #     :in_list => 6,
  #     :in_edit => false,
  #     :filterable => true,
  #     :sortable => true
  #   }
  #
  #   has_column "name",{
  #     :ajax => true,
  #     :in_list => true,
  #     :sortable => :desc,
  #     :filterable =>true
  #   }
  #
  #   has_column "phone",{
  #     :description => "(Format: ###-###-####)", 
  #     :in_list => 10,
  #     :sortable => true,
  #     :filterable =>true
  #   }
  #
  #   has_column "company_type_id",{
  #      :eval => :company_type_id_list,
  #      :filterable => true,
  #      :in_list => true,
  #      :lookup_eval => "CompanyType.find(:all).collect {|u| [ u.name, u.id ] }",
  #      :sortable => true,
  #      :type => :lookup
  #    }
  #
  #   has_column "count", {
  #     :eval => "one_row.contacts.size.to_s",
  #     :in_edit => false,
  #     :in_list => true
  #   }
  #
  #   has_column "name_key", {
  #     :label => "English Name",
  #     :filterable => true,
  #     :in_list => true,
  #     :sortable => false,
  #     :type => :custom
  #   }
  #
  # Helper Examples when <tt>:eval => :company_type_id_list</tt>
  # 
  #   def company_type_id_list(one_row)
  #     one_row.company_type.name
  #   end
  #
  # Helper Examples when <tt>:type => :custom</tt>
  #
  #   def name_key_edit(one_row, column)
  #     select one_row, column.name , StaticString.find(:all, :conditions => [ "static_strings.string_key LIKE 'LOCALE%%'" ]).collect {|ss| [ ss.string_text, ss.string_key ] }
  #   end
  #
  #   def name_key_list(one_row)
  #     one_row.name.string_text
  #   end
  #
  #   def name_key_filter(one_row, column, filter_by)
  #     select filter_by, column.name , StaticString.find(:all, :conditions => [ "static_strings.string_key LIKE 'LOCALE%%'" ]).collect {|ss| [ ss.string_text, ss.string_key ] }, :include_blank => true
  #   end
  def self.has_column(name, options = {})
    @scaffold_columns ||= []
    @scaffold_columns_hash ||= {}
    options[:name] = name
    scaffold_column = DryScaffold::ScaffoldColumn.new(self, options)
    @scaffold_columns << scaffold_column
    @scaffold_columns_hash[name] = scaffold_column
  end
  
  @default_per_page = nil
  def self.show_per_page(value) #:nodoc:
    @default_per_page = value
  end
  
  def self.default_per_page #:nodoc:
    @default_per_page.nil? ? 5 : @default_per_page
  end
  
  @scaffold_columns = nil
  def self.scaffold_columns #:nodoc:
    @scaffold_columns ||= build_scaffold_columns
  end
     
  @scaffold_columns_hash = nil
  def self.scaffold_columns_hash #:nodoc:
    @scaffold_columns_hash ||= build_scaffold_columns_hash
  end

  @child_models = nil
  def self.children #:nodoc:
    if @child_models.nil?
      []
    else
      @child_models
    end
  end

  # Formulates the relationships to be managed. 
  #
  # Because not all <tt>has_many</tt> or <tt>belongs_to</tt> relationships defined in the model are to be managed by DryScaffold, 
  # this method restricts the management to only those models identified.
  #
  # Example: 
  #   has_many :contacts, :dependent => true
  #   has_many :comments, :dependent => true
  #   has_child :contacts
  #   has_child :comments
  def self.has_child(model)
    # We store this as an array so that it can be ordered.
    @child_models ||= []
    case model
      when String
        @child_models << model.camelize.singularize.constantize
      when Symbol
        @child_models << model.to_s.camelize.singularize.constantize
      else
        @child_models << model
    end
#    @inherited = true
  end
  
  # A shortcut method to <tt>has_child</tt>.
  #
  # Example: 
  #   has_many :contacts, :dependent => true
  #   has_many :comments, :dependent => true
  #   has_many :statuses, :dependent => true
  #   has_children [:contacts, :comments, :statuses]
  def self.has_children(models = [])
    models.each do |model|
      has_child model
    end
  end

  def self.has_children? #:nodoc:
    not @child_models.nil?
  end

  @crud = nil
  # Primarily used to disable CRUD, sort, filter and paging actions Model-wide. This value will override 
  # any <tt>has_column</tt> values. Where applicable, it is also possible to specify a label other than the default.
  #
  # Options are:
  # * <tt>:creatable</tt> - Default value is +true+. Default label is +Create+.
  # * <tt>:deletable</tt> - Default value is +true+. Default label is +Delete+.
  # * <tt>:editable</tt> - Default value is +true+. Default label is +Edit+.
  # * <tt>:showable</tt> - Default value is +true+. Default label is +Show+.
  # * <tt>:filterable</tt> - Default value is +true+.
  # * <tt>:viewable</tt> - Default value is +true+. Default label is +View+.
  # * <tt>:pageable</tt> - Default value is +true+. (Display paging controls for this Model)
  # * <tt>:sortable</tt> - Default value is +true+.
  #
  # Example: 
  #   has_crudability :viewable => "Contacts", :deletable => false, :filterable => false
  def self.has_crudability(options)
    @crud = options
  end
  
  def self.crud #:nodoc:
    if @crud.nil?
      {}
    else
      @crud
    end
  end
  
  def self.is_crudable?(key) #:nodoc:
    if crud.has_key?(key) 
      crud[key]
    else
      true
    end
  end

  def self.crudable_label(key, default = "") #:nodoc:
    crud.has_key?(key) ? (crud[key].is_a?(String) ? crud[key] : default) : default
  end
  
  def self.filterable? #:nodoc:
    is_crudable?(:filterable)
  end
  
  def self.sortable? #:nodoc:
    is_crudable?(:sortable)
  end
  
  # At some level this just makes my life easier, I don't have to do the <tt>reflect_on_all_associations</tt> walk trying to
  # read your mind.
  #
  # Example: 
  #   belongs_to :company
  #   belongs_to :corporation
  #   has_parent :company
  def self.has_parent(model)
    @parent_model = model.to_s.camelize
#FIXME 2006-06-26 Level=1 - Set inherited based on some real evaluation
#    @inherited = true
  end
  
  def self.has_parent? #:nodoc:
    !@parent_model.nil?
  end
  
  def self.base_model #:nodoc:
    if @inherited.nil? or @inherited == false
      self.to_s.downcase.to_sym
		else
      self.to_s.camelize.constantize.superclass.to_s.underscore.to_sym
		end    
  end
  
  def self.url_model #:nodoc:
    if has_parent?
      @parent_model
    else
      name
    end
  end

  def self.url_sub_model #:nodoc:
    if has_parent?
      name
    else
      nil
    end
  end  

  def self.user_key #:nodoc:
    self.reflect_on_association(:user).nil? ? nil : self.reflect_on_association(:user).primary_key_name
  end
  
end
